Repository navigation
fix(semantic): bootstrap-complete archives a reopened replayed turn without duplicating it - #1391
Merged
Merged
Conversation
…ithout duplicating it #1290: the bootstrap-complete reconciler archived a still-open replayed turn with a raw append while every other path replaces by turnId, so a replayed turn already in history that reopened as currentTurn was held twice (duplicate sem:T:i candidates and semantic-block:T:i keys). It now uses appendSemanticHistory. Red before on the real subscription hub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n at bootstrap-complete #1391 review a: a replayed turn_started reopens T empty; if replay quiets before its content re-arrives, replacing by turnId swapped the full archived T for the empty copy and the answer vanished from the feed. archiveReplayedTurn keeps the archived row unless the replay copy renders at least as much (text and blocks); still one row per turnId. Live fold paths keep replace-by-id (a newer live copy is authoritative). Text and block rules each pinned; red before on the reviewer's sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y copy replaces the archived turn #1391 review a, round 2: a replay can reopen the same block EMPTY (block_started then quiet before text_delta), so equal block counts still let an empty block displace the archived answer. archiveReplayedTurn now compares renderable size: turn text plus each block's text, thinking, tool input and tool result. Each term, the equal-replaces rule and the reviewer's probe are pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…field (#1391 q93) Three size proxies for 'the replay covers the archive' failed review. The bootstrap path now merges: strings take the replay only when it extends the archived value, objects merge per key, id lists union. Tests go through the real ledger adapter, including both round-3 sequences and a mixed-progress case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eplay merge (#1391 review a round 4) Keeping archived lifecycle values blocked a replay that completed a block or tool the archive saw pending. status/messagePhase now rank, booleans OR, and the derived in-progress tool lists are recomputed after the merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
review a round 5, q95) resultContent/resultIsError/resultAt and the tool's lookup state now come together from the copy with the later resultAt (replay on a tie). Merged field by field, an archived error followed by a corrected result painted the success red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Review disposition (head Fixed
Residuals (stated, not blocking)
🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1290.
Plan:
docs/plans/2026-09-27-semantic-history-bootstrap-dedupe.md.Problem
The bootstrap-complete reconciler in
useIpcSubscriptions.tsarchived a still-open replayed turn with a raw[...history, row]append. A replayed turn T that was already in history could reopen ascurrentTurn, and the ledger hides that copy while T is live. Bootstrap-complete then appended T a second time. The result was two history rows with one turnId, repeatedsem:T:icandidate ids, and duplicatesemantic-block:T:iReact keys.What merges
The reconciler archives through a new
archiveReplayedTurnhelper insession-runtime/semantic/helpers.ts:appendSemanticHistory).status: 'completed'against a reopened, longer'in_progress'.blocks, each block,lookups,usage): merged key by key.blockOrder, the lookup id lists): unioned.statusranks pending < in_progress < terminal (two different terminal values, or an unknown value, keep the archived one);messagePhaseranks commentary < final_answer; booleans OR (finalized,inputJsonValidonly ever become true). This protects a completed archive from a reopened replay AND lets a replay complete a block or tool the archive saw pending (review a, round 4).resultContent,resultIsError,resultAtand the tool's lookup state (toolCallsById[id], and its membership inresolvedToolUseIds/erroredToolUseIds) all come from the copy with the LATERresultAt, and from the replay on a tie. Field by field, an archived error followed by a corrected result painted the success red, because the live reducer lets a later result clear the error, so no single result field is monotonic.task.inProgressToolUseIdsis recomputed after the merge (the union minus every resolved/errored id).activeToolNameskeeps only names both copies agree on.Invariant: every archived string survives at its path, either unchanged or extended, and every archived block index and id stays. No lifecycle field moves backwards, and none is held back when either copy has progressed. The ledger paints only from these fields, so a field visible before bootstrap-complete cannot vanish. Content the replay added, such as a new block or longer text, is kept too.
Why a merge and not a proxy: reviewer A rejected three "does the replay cover the archive?" proxies in rounds 1–3:
reasoningSummary, and let unpainted turn text outweigh a missing answer block.A merge doesn't have to judge which copy is richer.
Scope: live fold paths are unchanged. There, a newer copy of a turn stays authoritative (
appendSemanticHistoryreplace).Tests
archiveReplayedTurn.test.ts: 15 cases. Each feeds the helper's output through a real renderer function (createLedgerInputAdapter()for painted text,groupSemanticActivityfor tool activity). The cases:reasoningSummary, and longer turn text vs an empty answer block);status, Claudefinalized), and a Read tool the replay resolved, plus the reverse, checked through the realgroupSemanticActivity(not running) and the recomputed in-progress lists.f7b65430, 4 of the round-3 cases fail (both round-3 sequences, mixed progress, status). Atf6cae4ba, the 4 round-4 cases fail. At8aa9ab02, the 3 round-5 cases fail.resultIsErrornot taken;bootstrapHistoryDedupe.renderer.test.tsx: drives the realuseIpcSubscriptionsthrough the fake session feed. It checks two things:turn-Trow after bootstrap-complete (before the fix:['turn-T', 'turn-T']);turn_started/block_startedwith no deltas yet). Nothing here measures how often this happens.npx tsc -b+src/renderer/src/workspace/hook/ipc+src/renderer/src/session-runtime/semantic+src/renderer/src/features/feed+src/renderer/src/renderingon39c712b6: 58 files, 419 tests.🤖 Generated with Claude Code